'use strict'

/**
 * @private
 */
let allModels = Object.create(null);

/**
 * Get the registered model by model name.
 *
 * @public
 *
 * @param {String} modelName - The name of model
 *
 * @return {class<model/base/Model>}    The model whose name is matched with argument.
 *                      Return undefined if the model name is not registered.
 *
 * @example
 * let User = um.model.getModel('User');
 */
function getModel(modelName) {
    return allModels[modelName];
}

/**
 * Create the UniqueModel class from schema
 *
 * @public
 *
 * @param {String} modelName - The name of model.
 * @param {String} schema - The schema of model.
 *
 * @return {class<mode/base/Model>}    The model created according to the schema.
 *
 * @example
 * let Image = um.model.createModel('Image', {
 *     path: Types.Text(),
 *     type: Types.Text(),
 *     width: Types.Integer(),
 *     height: Types.Integer()
 * });
 *
 * let User = um.model.createModel('User', {
 *     name: Types.Text(),
 *     age: Types.Integer(),
 *     height: Types.Double(),
 *     weight: Types.Double(),
 *     photo: Types.UObject({
 *         type: 'Image'
 *     })
 * });
 */
function createModel(modelName, schema) {
    let um = require('..');
    let Base = require('./base');
    let BaseModel = Base.Model;
    let ModelStatus = BaseModel.Status;
    let Types = um.Types;

    // 模型类
    /**
     * The model class generated by createModel.
     * It will extend um/Base/Model.
     *
     * static _meta: 模型信息
     *     name: 模型名
     *     schema: 模型模式
     *     dao: 数据访问对象
     * 
     * @public
     */
    class Model extends BaseModel {
        constructor(values) {
            super();

            /**
             * The class of this model.
             *
             * @public
             * @type {class<um/base/Model} 
             */
            Object.defineProperty(this, 
                '_class', {
                    value: Model,
                    writable: false,
                    enumerable: false,
                    configurable: false,
            });

            /**
             * The object to store data of unique object.
             *
             * @public
             * @type {class<um/base/Model} 
             */
            Object.defineProperty(this, 
                '_data', {
                    value: Object.create(null),
                    writable: true,
                    enumerable: false,
                    configurable: false,
            });

            /**
             * The id of object.
             *
             * @public
             * @type {String} 
             */
            Object.defineProperty(this, 
                'id', {
                    get() {
                        return this._id;
                    },
                    set(id) {
                        /**
                         * The id of object. the id property will access this private property.
                         *
                         * @private
                         * @type {String} 
                         */
                        if ( id ) {
                            this._id = String(id);
                        }
                        else {
                            this._id = null;
                        }
                    },
                    enumerable: true,
                    configurable: false,
            });

            /**
             * The status of object.
             *
             * @public
             * @type {um/base/Model.Status} 
             */
            this._status = ModelStatus.New;

            /**
             * Specify whether the model has loaded completely.
             *
             * @public
             * @type {Boolean} 
             */
            this._complete = false;

            if ( values ) {
                um.Types.toUniqueModel(Model, values, this);
            }
        }

        /**
         * Convert the unique model object to string.
         *
         * @public
         * @return {String} The String representation of unique model object.
         */
        toString() {
            let value = Object.create(null);
            let schema = Model._meta.schema;
            Object.keys(schema).forEach(fieldName => {
                value[fieldName] = this[fieldName];
            });
            value.id = this.id;

            return value;
        }

        setDefaultValue(options, key, value) {
            if ( !(key in options) ) {
                options[key] = value;
            }
        }

        toObjectInclude(includes) {
            let object = Object.create(null);
            let schema = Model._meta.schema;

            includes.forEach(fieldInfo => {
                if ( fieldInfo instanceof Object ) {
                    let objectKeys = Object.keys(fieldInfo);
                    if ( objectKeys.length === 0 ) {
                        return;
                    }

                    let fieldName = Object.keys(fieldInfo)[0];
                    let subInclude = fieldInfo[fieldName];
                    let type = schema[fieldName];

                    if ( !type ) {
                        return;
                    }

                    let fieldValue = this[fieldName];
                    if ( !fieldValue ) {
                        return;
                    }

                    if ( type.name === 'UObject' ) {
                        fieldValue = fieldValue.toObjectInclude(subInclude);
                    }
                    else if ( type.name === 'UObjectArray' ) {
                        fieldValue = fieldValue.map(uniqueObject => {
                            return uniqueObject.toObjectInclude(subInclude);
                        });
                    }
                    
                    object[fieldName] = fieldValue;
                }
                else {
                    let fieldName = fieldInfo;
                    let type = schema[fieldName];
                    if ( !type ) {
                        return;
                    }

                    let fieldValue = type.toValue(this[fieldName]);

                    if ( fieldValue === null ) {
                        return;
                    }

                    object[fieldName] = fieldValue;
                }
            });

            object.id = this.id;
            return object;
        }

        toObject(options, cache) {
            if ( !options ) {
                options = {};
            }

            if ( 'includes' in options ) {
                return this.toObjectInclude(options.includes);
            }

            if ( !cache ) {
                cache = new Set();
            }

            this.setDefaultValue(options, 'recursive', false);

            // 判断是否回环
            if ( this.id ) {
                if ( cache.has(this.id) ) {
                    return {
                        id: this.id
                    };
                }

                // 加入缓存
                cache.add(this.id);
            }

            let object = Object.create(null);
            let schema = Model._meta.schema;

            Object.keys(schema).forEach(fieldName => {
                let type = schema[fieldName];
                let fieldValue = type.toValue(this[fieldName]);

                // 如果需要递归则递归解析
                if ( options.recursive ) {
                    if ( type.name === 'UObject' ) {
                        if ( !fieldValue ) {
                            return;
                        }
                        fieldValue = this[fieldName].toObject(options, cache);
                    }
                    else if ( type.name === 'UObjectArray' ) {
                        if ( !fieldValue ) {
                            return;
                        }
                        fieldValue = this[fieldName].map(uniqueObject => {
                            return uniqueObject.toObject(options, cache);
                        });
                    }
                }

                if ( fieldValue === null ) {
                    return;
                }

                object[fieldName] = fieldValue;
            });
            object.id = this.id;

            // 判断是否回环
            if ( this.id ) {
                // 删除缓存
                if ( cache.has(this.id) ) {
                    cache.delete(this.id);
                }

            }

            return object;
        }
    }

    Object.defineProperty(Model, 
        '_meta', {
            value: {
                name: modelName,
                schema: schema
            },
            writable: false,
            enumerable: false,
            configurable: false,
    });

    Model.toObjectArray = function(uniqeModelObjects, options) {
        return uniqeModelObjects.map(uniqueModelObject => {
            return uniqueModelObject.toObject(options);
        });
    }

    // Create the properties of object according to the fields of schema.
    Object.keys(schema).forEach(fieldName => {
        let fieldType = schema[fieldName];
        /**
         * Operate the property of object.
         *
         * @public
         * @type {*}
         */
        Object.defineProperty(Model.prototype, 
            fieldName, {
                get() {
                    // If the model is created by a persistent session
                    // and it has not loaded completely,
                    // then we refresh the object to load all data.
                    // Or we return the value of field directly.
                    if ( this._dao && this._complete === false ) {
                        this._dao.refresh(this);
                    }
                    return this._data[fieldName];
                },
                set(value) {
                    if ( this._dao && this._complete === false ) {
                        this._dao.refresh(this);
                    }
                    this._data[fieldName] = fieldType.fromValue(value);
                },
                enumerable: true,
                configurable: true
            }
        );
    });

    // Register the model using modelName.
    allModels[modelName] = Model;

    return Model;
}

module.exports = {
    createModel,
    getModel
};
